Completed
Push — master ( 7f06eb...e3cad9 )
by Simon
28s
created

fisherman.js ➔ ???   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 49

Duplication

Lines 0
Ratio 0 %

Importance

Changes 15
Bugs 0 Features 0
Metric Value
cc 2
c 15
b 0
f 0
nc 2
nop 1
dl 0
loc 49
rs 9.2258
1
/**
2
 * The main fisherman class, to create commands and interact with them
3
 * @author Maxerbox | Simon Sassi 2017
4
 * @extends {EventEmitter}
5
 * @class Fisherman
6
 */
7
const async = require('async')
8
const FisherRegister = require('./register.js')
9
const defaultFisherOpts = require('./util/FishermanOptions')
10
const escapeRegExp = require('./util/RegExpEscape')
11
var fisherRouter = require('./router/router')
12
const EventEmitter = require('events')
13
const fisherCodes = require('./util/FisherCodes')
14
const CommandNotFoundException = require('./exceptions/CommandNotFoundException')
15
const InvalidChannelException = require('./exceptions/InvalidChannelException')
16
const InvalidPatternException = require('./exceptions/InvalidPatternException')
17
const MissingPermissionsException = require('./exceptions/MissingPermissionsException')
18
19
class Fisherman extends EventEmitter {
20
    /**
21
     * Creates an instance of Fisherman.
22
     * @param {FishermanOptions} options The options for fisherman
23
     * @memberof Fisherman
24
     */
25
  constructor (options = {}) {
26
    super()
27
        /**
28
         * Used to instantiate the FisherRouter
29
         * @name Fisherman#fisherRouterPrototype
30
         * @type {FisherRouter}
31
         */
32
    this.fisherRouterPrototype = fisherRouter
33
        /**
34
         * The middleware handling function stack
35
         * @private
36
         * @name Fisherman#handleListeners
37
         * @type {Array}
38
         */
39
    this.handleListeners = []
40
        /**
41
         * The middleware function stack on init
42
         * @private
43
         * @name Fisherman#messageListeners
44
         * @type {Array}
45
         */
46
    this.setUpListeners = []
47
        /**
48
         * All the commands handled by fisherman
49
         * @name Fisherman#commands
50
         * @type {Map.<string, Command>}
51
         */
52
    this.commands = new Map()
53
        /**
54
         * All the command aliases handled by fisherman
55
         * @name Fisherman#aliases
56
         * @type {Map.<string, Command>}
57
         */
58
    this.aliases = new Map()
59
        /**
60
         * All the command registers handled by fisherman
61
         * @name Fisherman#registers
62
         * @type {Map.<string, FisherRegister>}
63
         */
64
    this.registers = new Map()
65
        /**
66
         * A fastfall empty callback
67
         * @private
68
         * @name Fisherman#fallHandle
69
         */
70
    this.fallHandle = require('fastfall')(this.handleListeners)
71
    this.setOptions(options)
72
    if (!this.client) { this.client = new (require('discord.js')).Client(this.clientOptions) }
73
  }
74
    /**
75
     * Set the options to fisherman
76
     *
77
     * @param {FishermanOptions} options
78
     * @memberof Fisherman
79
     */
80
  setOptions (options) {
81
    var opts = Object.assign(defaultFisherOpts, options)
82
    if (opts.client) { this.client = opts.client }
83
    if (opts.prefixes) { this.setPrefixe(opts.prefixes) }
84
    this.commandMatchRegExp = new RegExp(opts.commandMatchRegExp)
85
    this.ownerID = opts.ownerID
86
    this.clientOptions = options.clientOptions
87
    this.sendAliasStatus = opts.sendAliasStatus
88
    this.sendNotFoundStatus = opts.sendNotFoundStatus
89
    this.selfMessageProcessing = opts.selfMessageProcessing
90
  }
91
92
    /**
93
     *
94
     * Set the prefixe
95
     * @param {(Array|string)} prefixes
96
     * @memberof Fisherman
97
     */
98
  setPrefixe (prefixes) {
99
    if (typeof prefixes === 'string') {
100
      this.regPref = new RegExp('^(' + escapeRegExp.escapeString(prefixes) + ').*')
101
    } else if (Array.isArray(prefixes)) {
102
      var escapedArray = escapeRegExp.escapeArray(prefixes)
103
      this.regPref = new RegExp('^(' + escapedArray.join('|') + ').*')
104
    }
105
  }
106
    /**
107
     * Create a Fisherman instance from an already logged in discord.js client
108
     *
109
     * @static
110
     * @param {Client} client The discord.js client
111
     * @param {FishermanOptions} fisherOptions The options for Fisherman
112
     * @memberof Fisherman
113
     */
114
  static createFromClient (client, fisherOptions = {}) {
115
    var opts = Object.assign({ client: client }, fisherOptions)
116
    return new this(opts)
117
  }
118
119
    /**
120
     *
121
     * Message event listener
122
     * @private
123
     * @param {Message} message A discord.js Message
124
     * @memberof Fisherman
125
     */
126
  handleMessage (message) {
127
    if (message.author.id === this.client.user.id && !this.selfMessageProcessing) { return }
128
    var router = fisherRouter.buildFromMessage(this, message)
129
    var that = this
130
    var prefixe = router.request.prefix = this.checkPrefixe(message.content)
131
    try {
132
      router.request.command = prefixe ? this.checkCommand(prefixe, message) : null
133
      if (router.request.command) router.request.isCommand = true
0 ignored issues
show
Coding Style Best Practice introduced by
Curly braces around statements make for more readable code and help prevent bugs when you add further statements.

Consider adding curly braces around all statements when they are executed conditionally. This is optional if there is only one statement, but leaving them out can lead to unexpected behaviour if another statement is added later.

Consider:

if (a > 0)
    b = 42;

If you or someone else later decides to put another statement in, only the first statement will be executed.

if (a > 0)
    console.log("a > 0");
    b = 42;

In this case the statement b = 42 will always be executed, while the logging statement will be executed conditionally.

if (a > 0) {
    console.log("a > 0");
    b = 42;
}

ensures that the proper code will be executed conditionally no matter how many statements are added or removed.

Loading history...
134
    } catch (err) {
135
      router.response.sendCode(err.code, err)
136
      return
137
    }
138
    this.fallHandle(router.request, router.response, function (err, request, response) {
139
      if (err) return err === true ? undefined : router.response.sendCode(fisherCodes.MIDDLEWARE_FAILED, err)
140
      if (router.request.command) {
141
        var cmd = router.request.command
142
        that.matchSuffixe(cmd, cmd.suffixe, function (result) {
143
          if (result) {
144
            if (cmd.isPromise) {
145
              (new Promise(function (resolve, reject) {
146
                cmd.execute(router.request, router.response, resolve, reject)
147
              })).then(res => router.response.sendCode(fisherCodes.COMMAND_SUCESS, res)).catch(err => router.response.sendCode(fisherCodes.COMMAND_FAILED, err))
148
            } else {
149
              cmd.execute(router.request, router.response)
150
            }
151
          } else {
152
            router.response.sendCode(fisherCodes.INVALID_PATTERN, new InvalidPatternException(cmd.suffixe))
153
          }
154
        })
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
155
      }
156
    })
157
  }
158
159
    /**
160
     * Check if there is a command, throw exceptions
161
     * @private
162
     * @param {string} prefixe The command prefixe
163
     * @param {Message} message A discord.js message
164
     * @returns {Array}
165
     * @memberof Fisherman
166
     */
167
  checkCommand (prefixe, message) {
168
        /*
169
        Benchmark split vs regex match : https://jsperf.com/regex-vs-split/2
170
        Benchmark inline RegExp vs Stored RegExp : https://jsperf.com/regexp-indexof-perf/24
171
        */
172
    var textCmd = message.content.substring(prefixe.length).match(this.commandMatchRegExp)[0]
173
    var cmd = this.commands.get(textCmd) || this.aliases.get(textCmd)
174
    cmd = this.validateCommand(message, cmd, textCmd) ? cmd : null
175
    if (cmd) cmd.suffixe = textCmd ? message.content.substring(prefixe.length + textCmd.length + 1) : ''
0 ignored issues
show
Coding Style Best Practice introduced by
Curly braces around statements make for more readable code and help prevent bugs when you add further statements.

Consider adding curly braces around all statements when they are executed conditionally. This is optional if there is only one statement, but leaving them out can lead to unexpected behaviour if another statement is added later.

Consider:

if (a > 0)
    b = 42;

If you or someone else later decides to put another statement in, only the first statement will be executed.

if (a > 0)
    console.log("a > 0");
    b = 42;

In this case the statement b = 42 will always be executed, while the logging statement will be executed conditionally.

if (a > 0) {
    console.log("a > 0");
    b = 42;
}

ensures that the proper code will be executed conditionally no matter how many statements are added or removed.

Loading history...
176
    return cmd
177
  }
178
    /**
179
     *
180
     * @private
181
     * @param {command} cmd
182
     * @param {string} suffixe
183
     * @param {function} validate
184
     * @memberof Fisherman
185
     */
186
  matchSuffixe (cmd, suffixe, validate) {
187
    if (cmd.regPattern) {
188
      validate(cmd.regPattern.test(suffixe))
189
    } else if (cmd.patternCallback) {
190
      cmd.patternCallback.test(suffixe, validate)
191
    } else {
192
      validate(true)
193
    }
194
  }
195
    /**
196
     * Validate a command, throw exceptions
197
     * @private
198
     * @param {Message} message A discord.js message
199
     * @param {command} cmd the command
200
     * @returns {boolean}
201
     * @memberof Fisherman
202
     */
203
  validateCommand (message, cmd, textCmd) {
204
    if (!cmd && this.sendNotFoundStatus) { throw new CommandNotFoundException(textCmd) } else if (!cmd) { return false }
205
    if (cmd.channelType.indexOf(message.channel.type) === -1) { throw new InvalidChannelException(message.channel.type) }
206
    let permissions = message.guild ? message.channel.permissionsFor(this.client.user) : null
207
    if (permissions && permissions.has(cmd.discordPermRequired)) {
208
      let missing = permissions.missing(cmd.discordSpecialPerms)
209
      if (missing.length > 0) { throw new MissingPermissionsException(missing) }
210
    }
211
    return true
212
  }
213
    /**
214
     * Check if there is a prefixe in content
215
     * @private
216
     * @param {string} content
217
     * @returns {string}
218
     * @memberof Fisherman
219
     */
220
  checkPrefixe (content) {
221
    var result = this.regPref.exec(content)
222
        // return (Array.isArray(result)) ? result[1] : null;
223
    return result ? result[1] : null
224
  }
225
    /**
226
     * Initialize Fisherman and the middlewares
227
     * @param {string} [token = null] The token to log in with, optional if the client is already connected
0 ignored issues
show
Documentation Bug introduced by
The parameter [token does not exist. Did you maybe mean token instead?
Loading history...
228
     * @param {function} callback An optional callback to trigger when Fisherman is initialized
229
     * @memberof Fisherman
230
     * @fires Fisherman#initialized
231
     */
232
  init (token = null, callback) {
233
    /**
234
     *  Emitted when Fisherman and middlewares are initialized
235
     * @event Fisherman#initialized
236
     */
237
    this.client.on('message', this.handleMessage.bind(this))
238
    var that = this
239
    if (!token) {
240
      this.initializeMiddleware(callback)
241
    } else {
242
      this.client.login(token).then(() => {
243
        that.initializeMiddleware(callback)
244
      })
245
    }
246
  }
247
    /**
248
     * Initialize the middlewares
249
     * @private
250
     *
251
     * @memberof Fisherman
252
     */
253
  initializeMiddleware (callback) {
254
    var that = this
255
    async.parallel(this.setUpListeners, function (err) {
256
      if (err) { throw err }
257
      if (typeof callback === 'function') { callback() }
258
      that.emit('initialized')
259
    })
260
  }
261
262
    /**
263
     * Create a new register to add commands
264
     * @fires Fisherman#registerAdded
265
     * @param {string} keyName The register key value, to set in the registers map
266
     * @param {string} [registerName = null] The register's name
0 ignored issues
show
Documentation Bug introduced by
The parameter [registerName does not exist. Did you maybe mean registerName instead?
Loading history...
267
     * @param {string} [registerDescription = null] The register's description
0 ignored issues
show
Documentation Bug introduced by
The parameter [registerDescription does not exist. Did you maybe mean registerDescription instead?
Loading history...
268
     * @return {FisherRegister} Return a FisherRegister instance
269
     * @memberof Fisherman
270
     */
271
  createRegister (keyName, registerName = null, registerDescription = null) {
272
    /**
273
     * Emitted when a new register is added
274
     * @event Fisherman#registerAdded
275
     */
276
    var register = new FisherRegister(this, registerName || keyName, registerDescription)
277
    this.registers.set(keyName, register)
278
    return register
279
  }
280
    /**
281
     *
282
     * Add a middleware to Fisherman
283
     * @param {(function|Object)} middleware The middleware function|class
284
     * @return {Fisherman}
285
     * @memberof Fisherman
286
     */
287
  use (middleware) {
288
    if (typeof middleware !== 'object' && typeof middleware !== 'function') throw new TypeError('A middleware must be a function or an object')
0 ignored issues
show
Coding Style Best Practice introduced by
Curly braces around statements make for more readable code and help prevent bugs when you add further statements.

Consider adding curly braces around all statements when they are executed conditionally. This is optional if there is only one statement, but leaving them out can lead to unexpected behaviour if another statement is added later.

Consider:

if (a > 0)
    b = 42;

If you or someone else later decides to put another statement in, only the first statement will be executed.

if (a > 0)
    console.log("a > 0");
    b = 42;

In this case the statement b = 42 will always be executed, while the logging statement will be executed conditionally.

if (a > 0) {
    console.log("a > 0");
    b = 42;
}

ensures that the proper code will be executed conditionally no matter how many statements are added or removed.

Loading history...
289
    this.appendMiddleware(middleware)
290
    return this
291
  }
292
293
    /**
294
     *
295
     * Add a middleware to Fisherman
296
     * @private
297
     * @param {function} middleware The middleware function|class
298
     * @memberof Fisherman
299
     */
300
  appendMiddleware (middleware) {
301
    if (typeof middleware.setUp === 'function') { this.setUpListeners.push(middleware.setUp.bind(middleware, this)) }
302
    if (typeof middleware.handle === 'function') {
303
      this.handleListeners.push(middleware.handle.bind(middleware))
304
      this.fallHandle = require('fastfall')(this.handleListeners)
305
    }
306
  }
307
}
308
module.exports = Fisherman
309